import { FlowPreInput, WorkflowError } from './workflow';
import { ActionCallback, FlowAction, FlowActionInfo } from './action';
import { FlowPlayer, FlowPlayerInfo, UserPlayer, OrganPlayer, RolePlayer, CustomPlayer, Players } from './player';
import { ArriveContext } from './context';
import { FlowInstanceInfo } from './instance';

function toActionsInfo(actions: FlowAction[]): FlowActionInfo[] {
	return actions.map((a) => FlowAction.toActionInfo(a));
}
function toPlayersInfo(players: FlowPlayer[]): FlowPlayerInfo[] {
	return players.map((p) => FlowPlayer.toPlayerInfo(p));
}

export type NodeType = 'begin' | 'normal' | 'end';

export interface FlowNodeInfo {
	readonly id: string;
	readonly name: string;
	readonly type: NodeType;
	/**
	 * If true then approver must pick user from candidates manully,
	 * if false every user、role or organ users on this node will receive todo notify.
	 */
	readonly choosePlayer: boolean;
	/**
	 * The maximum count of the players that user can pick, used only when choosePlayer is true.
	 */
	readonly maxPlayers: number;
	readonly players: FlowPlayerInfo[];
	readonly actions: FlowActionInfo[];
}

type ArriveHandler = (context: ArriveContext) => Promise<void>;

export abstract class FlowNode {
	public flowId: string | null = null;
	/**
	 * If true then approver must pick user from candidates manully,
	 * if false every user、role or organ users on this node will receive todo notify.
	 */
	choosePlayer = true;
	/**
	 * The maximum count of the players that user can pick, used only when choosePlayer is true.
	 */
	maxPlayers = 1;
	abstract readonly type: NodeType;
	onArrive?: ArriveHandler;
	constructor(public readonly id: string, public readonly name: string) {}
	let(proc: (node: this) => void) {
		proc(this);
		return this;
	}
	static isBegin(node: FlowNode): node is BeginNode {
		return node.type === 'begin';
	}
	static isNormal(node: FlowNode): node is NormalNode {
		return node.type === 'normal';
	}
	static isEnd(node: FlowNode): node is EndNode {
		return node.type === 'end';
	}
	static isBeginOrNormal(node: FlowNode): node is NormalNode | BeginNode {
		return node.type === 'begin' || node.type === 'normal';
	}

	static toNodeInfo(node: FlowNode): FlowNodeInfo {
		return {
			type: node.type,
			id: node.id,
			name: node.name,
			choosePlayer: node.choosePlayer,
			maxPlayers: node.maxPlayers,
			players: FlowNode.isBeginOrNormal(node) ? toPlayersInfo(node.players) : [],
			actions: FlowNode.isBeginOrNormal(node) ? toActionsInfo(node.actions) : [],
		};
	}
}
abstract class BeginOrNormal extends FlowNode {
	players: FlowPlayer[] = [];
	actions: FlowAction[] = [];
	getAction(id: string): FlowAction {
		const action = this.actions.find((a) => a.id === id);
		if (!action) throw new WorkflowError(this.flowId ?? '', `action [${id}] on node [${this.id}] not exist`);
		return action;
	}
	onArrive?: ArriveHandler;
	addPlayer(player: FlowPlayer): this {
		this.players.push(player);
		return this;
	}
	addUserPlayer(id: string, name: string): UserPlayer {
		const player = new UserPlayer(id, name);
		this.addPlayer(player);
		return player;
	}
	addOrganPlayer(id: string, name: string, withSubOrgan: boolean): OrganPlayer {
		const player = new OrganPlayer(id, name, withSubOrgan);
		this.addPlayer(player);
		return player;
	}
	addRolePlayer(id: string, name: string): RolePlayer {
		const player = new RolePlayer(id, name);
		this.addPlayer(player);
		return player;
	}
	addCustomPlayer(
		id: string,
		name: string,
		getPlayers: (instance: FlowInstanceInfo, input: FlowPreInput) => Players | Promise<Players>,
		isValidPlayer: (user: UserPlayer) => boolean | Promise<boolean>
	): CustomPlayer {
		const player = new CustomPlayer(id, name, getPlayers, isValidPlayer);
		this.addPlayer(player);
		return player;
	}
	addAction(id: string, name: string, nextNodes: string[], choosePlayer = true, onAction?: ActionCallback) {
		if (this.actions.find((a) => a.id === id)) {
			throw new WorkflowError(this.flowId ?? '', `action [${id}] on node [${this.id}] already exist`);
		}
		const action = new FlowAction(id, name, nextNodes, choosePlayer, onAction);
		this.actions.push(action);
		return action;
	}
}

export class NormalNode extends BeginOrNormal {
	override get type(): 'normal' {
		return 'normal';
	}
}

export class BeginNode extends BeginOrNormal {
	override get type(): 'begin' {
		return 'begin';
	}
}

type EndArriveContext = Exclude<ArriveContext, 'go'>;
type EndArriveHandler = (context: EndArriveContext) => Promise<void>;

export class EndNode extends FlowNode {
	override get type(): 'end' {
		return 'end';
	}
	override onArrive?: EndArriveHandler;
}
